package it.uniroma2.art.owlart.utilities.transform;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

import it.uniroma2.art.owlart.exceptions.ModelAccessException;
import it.uniroma2.art.owlart.exceptions.ModelUpdateException;
import it.uniroma2.art.owlart.model.ARTNode;
import it.uniroma2.art.owlart.model.ARTStatement;
import it.uniroma2.art.owlart.model.ARTURIResource;
import it.uniroma2.art.owlart.model.NodeFilters;
import it.uniroma2.art.owlart.models.RDFModel;
import it.uniroma2.art.owlart.navigation.ARTStatementIterator;

/**
 * This class implements a rule based engine for transforming an RDF graph. The processing is specified
 * through a chain of objects implementing the interface {@link Operation}. Each statement goes through the
 * chain, until an operation returns true. Usually, you would append either the operation {@link #RETAIN} or
 * an instance of {@link DiscardOperation}.
 * 
 * @author <a href="fiorelli@info.uniroma2.it">Manuel Fiorelli</a>
 * 
 */
public class RDFTransformer {
	/**
	 * An operation for transforming an RDF statement.
	 * 
	 */
	public static interface Operation {
		/**
		 * 
		 * @param sourceModel
		 *            the source model
		 * @param targetModel
		 *            the target model
		 * @param stmt
		 *            the statement to be transformed
		 * @return {@code true} iff the processing of a statement is finished
		 * @throws Exception
		 */
		boolean execute(RDFModel sourceModel, RDFModel targetModel, ARTStatement stmt) throws Exception;
	};

	/**
	 * A predicate over RDF statements.
	 */
	public static class StatementPredicate implements Predicate<ARTStatement> {
		private static final Predicate<ARTNode> TRUE = Predicates.alwaysTrue();

		private Predicate<ARTNode> subjPredicate;
		private Predicate<ARTNode> predPredicate;
		private Predicate<ARTNode> objPredicate;

		public StatementPredicate(Predicate<ARTNode> subjPredicate, Predicate<ARTNode> predPredicate,
				Predicate<ARTNode> objPredicate) {
			this.subjPredicate = subjPredicate;
			this.predPredicate = predPredicate;
			this.objPredicate = objPredicate;
		}

		public StatementPredicate() {
			this(TRUE, TRUE, TRUE);
		}

		public StatementPredicate withSubject(Predicate<ARTNode> predicate) {
			this.subjPredicate = predicate;
			return this;
		}

		public StatementPredicate withPredicate(Predicate<ARTNode> predicate) {
			this.predPredicate = predicate;
			return this;
		}

		public StatementPredicate withObject(Predicate<ARTNode> predicate) {
			this.objPredicate = predicate;
			return this;
		}

		public boolean apply(ARTStatement stmt) {
			return subjPredicate.apply(stmt.getSubject()) && predPredicate.apply(stmt.getPredicate())
					&& objPredicate.apply(stmt.getObject());
		}
	}

	/**
	 * A predicate that filters bnodes.
	 */
	public static class BNodePredicate implements Predicate<ARTNode> {
		public boolean apply(ARTNode node) {
			return node.isBlank();
		}
	}

	/**
	 * A statement that matches a given URI.
	 */
	public static class URIPredicate implements Predicate<ARTNode> {
		private ARTURIResource uri;

		public URIPredicate withURI(ARTURIResource uri) {
			this.uri = uri;
			return this;
		}

		public boolean apply(ARTNode node) {
			boolean t = node.isURIResource();

			if (t) {
				if (uri != null) {
					t = node.asURIResource().equals(uri);
				}
			}

			return t;
		}
	}

	/**
	 * A predicate over RDF literals.
	 */
	public static class LiteralPredicate implements Predicate<ARTNode> {
		private Pattern lexicalForm;
		private Pattern lang;

		public LiteralPredicate withLexicalForm(String lexicalForm) {
			this.lexicalForm = Pattern.compile("^" + Pattern.quote(lexicalForm) + "$");
			return this;
		}

		public LiteralPredicate withLang(String lang) {
			this.lang = Pattern.compile("^" + Pattern.quote(lang) + "$");
			return this;
		}

		public LiteralPredicate withLexicalForm(Pattern lexicalForm) {
			this.lexicalForm = lexicalForm;
			return this;
		}

		public LiteralPredicate withLang(Pattern lang) {
			this.lang = lang;
			return this;
		}

		public boolean apply(ARTNode node) {
			boolean t = node.isLiteral();

			if (t) {
				if (lexicalForm != null) {
					t = node.asLiteral().getLabel().equals(lexicalForm);
				}

				if (t) {
					if (lang != null) {
						Matcher m = lang.matcher(node.asLiteral().getLanguage());
						t = m.matches();
					}
				}
			}

			return t;
		}
	}

	/**
	 * An abstract implementation of the interface {@link Operation} that processes a given statement
	 * providing that it satisfies a given predicate.
	 */
	public static abstract class AbstractOperation implements Operation {

		private static final Predicate<ARTStatement> TRUE = Predicates.alwaysTrue();

		private Predicate<ARTStatement> predicate;

		protected AbstractOperation(Predicate<ARTStatement> predicate) {
			this.predicate = predicate;
		}

		public AbstractOperation() {
			this(TRUE);
		}

		public boolean execute(RDFModel sourceModel, RDFModel targetModel, ARTStatement stmt) {
			if (predicate.apply(stmt)) {
				return doTransform(targetModel, targetModel, stmt);
			} else {
				return false;
			}
		}

		protected abstract boolean doTransform(RDFModel sourceModel, RDFModel targetModel, ARTStatement stmt);
	}

	/**
	 * Concrete operation that discards a statement.
	 */
	public static class DiscardOperation extends AbstractOperation {
		public DiscardOperation(Predicate<ARTStatement> predicate) {
			super(predicate);
		}

		@Override
		protected boolean doTransform(RDFModel sourceModel, RDFModel targetModel, ARTStatement stmt) {
			return true;
		}
	};

	/**
	 * Concrete operation that retains an explicit statement in the main graph.
	 */
	public static Operation RETAIN = new Operation() {

		public boolean execute(RDFModel sourceModel, RDFModel targetModel, ARTStatement stmt)
				throws ModelUpdateException, ModelAccessException {
			if (sourceModel.hasStatement(stmt, false, NodeFilters.MAINGRAPH)) {
				targetModel.addStatement(stmt, NodeFilters.MAINGRAPH);
			}
			return true;
		}
	};

	/**
	 * Executes the transformation.
	 * 
	 * @param sourceModel
	 *            the source model
	 * @param targetModel
	 *            the target model
	 * @throws Exception
	 */
	public void doTransformation(RDFModel sourceModel, RDFModel targetModel) throws Exception {
		ARTStatementIterator it = sourceModel.listStatements(NodeFilters.ANY, NodeFilters.ANY,
				NodeFilters.ANY, true, NodeFilters.MAINGRAPH);

		try {
			while (it.hasNext()) {
				ARTStatement stmt = it.getNext();
				transformStatement(sourceModel, targetModel, stmt);
			}
		} finally {
			it.close();
		}
	}

	private List<Operation> operations = new ArrayList<RDFTransformer.Operation>();

	/**
	 * Appends an operation to the processing chain.
	 * 
	 * @param operation
	 *            the operation to be added
	 */
	public void appendOperation(Operation operation) {
		operations.add(operation);
	}

	/**
	 * Removes an operation from the processing chain.
	 * 
	 * @param operation
	 *            the operation to be removed
	 * @return the removed operation
	 */
	public boolean removeOperation(Operation operation) {
		return operations.remove(operation);
	}

	private void transformStatement(RDFModel sourceModel, RDFModel targetModel, ARTStatement stmt)
			throws Exception {
		for (Operation op : operations) {
			boolean finished = op.execute(sourceModel, targetModel, stmt);
			if (finished == true) {
				break;
			}
		}
	}
}
